#!/usr/bin/env python
# -*- coding: utf-8 -*-
# http://docs.python.org/whatsnew/pep-328.html
from __future__ import absolute_import
from __future__ import print_function   # deletes the print statement
from __future__ import unicode_literals # unicode by default

import os, sys, cherrypy
from math import ceil

if cherrypy.__version__.split(".") < ["3", "1", "0"]:
    raise ImportError("Version 3.1 or later of CherryPy required.")

from cherrypy import HTTPRedirect as redirect, Application
import cherrypy
from unipath import Path
from .packages import PackageManager



def paginate(items, page=1, max_per_page=10):
    """Simple generic pagination.
    
    This function was written by Christopher Lenz, stolen from
    http://genshi.edgewall.org/wiki/HelperFunctions
    and then tweaked a little.
    
    Given an iterable, this function returns:
     * the slice of objects on the requested page,
     * the total number of items, and
     * the total number of pages.
    
    The `items` parameter can be a list, tuple, or iterator:
    
    >>> items = range(12)
    >>> items
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
    >>> paginate(items)
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
    >>> paginate(items, page=2)
    ([10, 11], 12, 2)
    >>> paginate(iter(items))
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
    >>> paginate(iter(items), page=2)
    ([10, 11], 12, 2)
    
    This function also works with generators:
    
    >>> def generate():
    ...     for idx in range(12):
    ...         yield idx
    >>> paginate(generate())
    ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
    >>> paginate(generate(), page=2)
    ([10, 11], 12, 2)
    
    The `max_per_page` parameter can be used to set the number of items that
    should be displayed per page:
    
    >>> items = range(12)
    >>> paginate(items, page=1, max_per_page=6)
    ([0, 1, 2, 3, 4, 5], 12, 2)
    >>> paginate(items, page=2, max_per_page=6)
    ([6, 7, 8, 9, 10, 11], 12, 2)
    """
    start = (page-1) * max_per_page
    stop = start + max_per_page
    
    count = None
    if hasattr(items, '__len__'):
        count = len(items)
    elif hasattr(items, 'count'): # SQLAlchemy query objects have .count()
        count = items.count()     # which lazily calls count on the query
    
    try: # Try slicing first for better performance
        retval = items[start:stop]
    except TypeError: # Slicing not supported, iterate through the whole list
        retval = []
        for idx, item in enumerate(items):
            if start <= idx < stop:
                retval.append(item)
            # If we already obtained the total number of items via `len()`,
            # we can break out of the loop as soon as we've got the last item
            # for the requested page
            if count is not None and idx >= stop:
                break
        if count is None:
            count = idx + 1
    return retval, count, int(ceil(float(count) / max_per_page))

def group(iterable, num, predicate=None):
    """Combines the elements produced by the given iterable so that every `num`
    items are returned as a tuple.
    
    This function was written by Christopher Lenz, stolen from
    http://genshi.edgewall.org/wiki/HelperFunctions
    
    >>> items = [1, 2, 3, 4]
    >>> for item in group(items, 2):
    ...     print item
    (1, 2)
    (3, 4)
    
    The last tuple is padded with `None` values if its' length is smaller than
    `num`.
    
    >>> items = [1, 2, 3, 4, 5]
    >>> for item in group(items, 2):
    ...     print item
    (1, 2)
    (3, 4)
    (5, None)
    
    The optional `predicate` parameter can be used to flag elements that should
    not be packed together with other items. Only those elements where the
    predicate function returns True are grouped with other elements, otherwise
    they are returned as a tuple of length 1:
    
    >>> items = [1, 2, 3, 4]
    >>> for item in group(items, 2, lambda x: x != 3):
    ...     print item
    (1, 2)
    (3,)
    (4, None)
    """
    buf = []
    for item in iterable:
        flush = predicate and not predicate(item)
        if buf and flush:
            buf += [None] * (num - len(buf))
            yield tuple(buf)
            del buf[:]
        buf.append(item)
        if flush or len(buf) == num:
            yield tuple(buf)
            del buf[:]
    if buf:
        buf += [None] * (num - len(buf))
        yield tuple(buf)



sa_pool = {} # a dictionary to store the SQLAlchemy engines

def enable_evalexception(app):
    from paste import evalexception
    app.merge({b"/": {
        b"wsgi.pipeline": (('evalexc', evalexception.EvalException),),
        b"wsgi.evalexc.global_conf": {},
        b"wsgi.evalexc.xmlhttp_key": "_xml",
    }})
    cherrypy.config.update({b"global":{b"request.throw_errors": True}})

def enable_evalexception_if_configured(app):
    s = cherrypy.config.get('on_error', "normal_cherrypy")
    if s == "evalexception":  enable_evalexception(app)
    return s



class App(Application):
    """A Webpyte application."""
    def __init__(self, app_module, mountpoint, env,
                 use_routes=True, use_sessions=True):
        self.env = cherrypy.env = env
        self.mountpoint = mountpoint
        if isinstance(app_module, basestring):
            exec "import %s as app_module" % app_module
        self.app_module = app_module
        root_controller = None if use_routes else app_module.Root()
        super(App, self).__init__(root=root_controller, script_name=mountpoint)
        self.directory = Path(app_module.__file__).absolute().parent
        # Configure
        self.merge({b"/": {
                b"tools.staticdir.root": self.directory,
                b"tools.staticfile.root": self.directory,
                b"tools.decode.on": True,
            }})
        directory_config = self.directory.child("config")
        if directory_config.isdir():
            self.load_config_file(directory_config, "main.ini")
            if env == "interpreter": # force loading development config
                s = raw_input(mountpoint.ljust(14) + ' - Load [p]roduction or [d]evelopment configuration? ')
                if s.startswith('p'):
                    self.load_config_file(directory_config, "prod.ini")
                else:
                    self.load_config_file(directory_config, "dev.ini")
            else:
                self.load_config_file(directory_config, env + ".ini")
        if use_sessions:  self._use_sessions()
        # Variables commonly used in templates; apps put more variables in it:
        # wp_routes adds url() to it:
        import webpyte
        self.defaultdict = dict(
            app = self,
            has_flash = webpyte.has_flash,
            flash = webpyte.consume_flash,
            paginate = paginate,
            group = group,
        )
        if use_routes:
            from cherrypy._cpdispatch import RoutesDispatcher
            dispatch = RoutesDispatcher()
            self.config["/"]["request.dispatch"] = dispatch
            self.dispatch = dispatch
            from .wp_routes import install_url_and_redirect
            install_url_and_redirect(self)
        if env != "interpreter":
            self._start_mailer_if_configured()
    
    def totemplate(self, **kw):
        """Creates the dictionary to be returned from a controller to
        the template, starting from a few commonly used variables.
        These can be overwritten by the variables returned by the controller.
        """
        d = self.defaultdict.copy()
        d.update(kw)
        return d
    
    def start(self):
        if hasattr(self.app_module, "on_start"):
            self.app_module.on_start(self)
    
    def load_config_file(self, *path):
        file = "/".join(path)
        self.merge(file)
        #cherrypy._cpconfig.merge(self.config, file)
    
    def use_genshi(self):
        from .gensher import Gensher
        self.gensher = Gensher(self)
        # If this app has a "templates" directory, use it with genshi
        d = self.directory.child("templates")
        if d.isdir():  self.gensher.add_search_path(d)
    
    def _start_mailer_if_configured(self):
        #c = self.config.get("mailer", {})
        # if not c.get("mail.on"):  return False
        from .mailer import Mailer
        self.mailer = Mailer()  #  Makes app.mailer available.
    
    def _use_sessions(self):
        '''Call this after the application configuration is loaded.
        It will fill the session configuration where it is void.
        When the final storage_type == 'file', it ensures existence of the
        sessions directory.
        '''
        conf = self.config.get('/', {})
        on = conf.setdefault(b'tools.sessions.on', True)
        timeout = conf.setdefault(b'tools.sessions.timeout', 120)
        storage_type = \
            conf.setdefault(b'tools.sessions.storage_type', 'file')
        if storage_type == 'file':
            storage_path = conf.setdefault(b'tools.sessions.storage_path',
                                           self.directory.child("sessions"))
            Path(storage_path).mkdir(parents=True) # ensure sessions dir exists



class Loader(object):
    envs = ("dev", "prod", "interpreter")
    
    def setup_i18n(self):
        # Install gettext, for internationalization
        # This feature is still under development!
        import gettext
        gettext.install("webpyte", "./locale", unicode=True)
    
    def set_error_handler(self, app):
        """In your global config files you can set one of these:
            
            on_error = "normal_cherrypy"
            on_error = "evalexception"
            on_error = "mailto:your@email.com"  # good for production
        
        The first alternative (default) gives you normal CherryPy behaviour.
        
        The second one uses Ian Bicking's evalexception, which is nice for
        development, because you can examine the stacktrace in a Python shell.
        
        The third one is for production. It requires the [mailer] section
        to be configured (app config). It emails the exception to you.
        On unexpected errors, webpyte.mailer.Mailer.handle_error()
        will run. See its docstring.
        """
        s = enable_evalexception_if_configured(app)
        if s.startswith("mailto:") and hasattr(app, "mailer"):
            app.mailer.developer_email = s[7:]
            app.merge({b"/":
                {b'request.error_response': app.mailer.handle_error}})
    
    def make_stop_scripts(self):
        """Writes a script that kills the current process - a convenient
        way of stopping the webserver.
        """
        pid = os.getpid()
        scriptname = "stop_server"
        msg = "Writing script: " + scriptname
        print(msg)
        #cherrypy.log(msg, context='', severity=logging.DEBUG, traceback=False)
        f = open(scriptname, "w")
        f.write("""#!/bin/sh

echo "Stopping the webserver (pid %i)."
kill %i""" % (pid, pid))
        f.close()
        
        scriptname = "kill_server"
        msg = "Writing script: " + scriptname
        print(msg)
        #cherrypy.log(msg, context='', severity=logging.DEBUG, traceback=False)
        f = open(scriptname, "w")
        f.write("""#!/bin/sh

echo "KILLING the webserver (pid %i)!"
kill -9 %i""" % (pid, pid))
        f.close()
    
    
    def __init__(self, env):
        self.setup_i18n()
        self.env = env
        self.virtual_hosts = {}
        self.apps = {}
        self.pacman = PackageManager()
        if env != "interpreter":
            # Read the global (server) configuration
            cherrypy.config.update(env + ".ini")
    
    def ask_which_app(self, appnames):
        if appnames is not list: appnames = list(appnames)
        c = 0
        for name in appnames:
            c = c + 1
            print(str(c).rjust(2) + ". " + name)
        opt = int(raw_input("Which app do you wish to work with? "))
        try:
            app = appnames[opt - 1]
        except:
            print("Invalid option. Bye.")
            sys.exit(4)
        return app
    
    def load(self, apps_args):
        """Takes a parameter *apps_args* which is a dictionary whose keys are
        application names and whose values are dictionaries containing
        parameters to the config_app method.
        
        Imports and mounts all the applications thus specified.
        
        As a side effect, the apps dictionary is populated.
        """
        if self.env == 'interpreter':
            appname = self.ask_which_app(apps_args.iterkeys())
            self.apps[appname] = app = self.config_app(**apps_args[appname])
            self.work_with(app)
            return
        # If not interpreter, load all specified apps
        for k, v in apps_args.iteritems():
            self.apps[k] = self.config_app(**v)
    
    def config_app(self, app_module, mountpoint,
                   use_routes=True, use_genshi=True, use_auth=True):
        """Mounts an app, applies its configuration files, calls its
        on_server_start() function, and sets some attributes in it.
        
        *app_module* is the module (or module name) on which the app is based.
        Example: "myblog"
        
        *mountpoint* is the place in the CherryPy tree where the app will be
        available; for instance, "/blog".
        
        In the application configuration files you may specify *domains*,
        *redirecting_domains* and *packages*. We discuss each below:
        
        If *domains* are given, this server becomes a virtual host, serving
        more than one app. In this case, each incoming request is handled
        to the correct app based on the domain name. For development with
        more than one app accessible from the root, you may have to open the
        /etc/hosts file and edit the localhost entry, adding a development
        host name. Example: domains=("oui.com.br", "ouidevelopment:9848")
        
        *redirecting_domains* lets you add other domains which, when accessed,
        redirect to the main domain ("oui.com.br" in the previous example).
        Example: redirecting_domains=("www.oui.com.br",)
        
        *packages* is the list of Webpyte packages that you want to make
        available for this app. Example: packages=("content", "cat", "contact")
        """
        app = App(app_module, mountpoint, self.env, use_routes)
        app = cherrypy.tree.mount(app, mountpoint)
        if use_genshi:  app.use_genshi()
        # Get the domains from config and prepare the dispatcher dict
        conf = app.config.get('/', {})
        domains = conf.get('domains', ())
        if isinstance(domains, basestring):  domains = (domains,)
        for d in domains:  self.virtual_hosts[d] = app
        # Redirecting domains are associated not with an app but with the
        # main domain:
        redirecting_domains = conf.get('redirect', ())
        if isinstance(redirecting_domains, basestring):
            redirecting_domains = (redirecting_domains,)
        for d in redirecting_domains:  self.virtual_hosts[d] = domains[0]
        if use_auth:
            from .auth import on_start
            on_start(app)
        self.pacman.addtoapp(app, conf.get('packages', None))
        self.set_error_handler(app)
        app.start()
        return app
    
    
    def work_with(self, app):
        '''Allows a console user to select which application to work with.'''
        cherrypy.request.app = app
        from .sqlalchemy_tool import configure_session_for_app
        configure_session_for_app(app)
        print("Working with: " + app.mountpoint)
    
    def start(self):
        """start() should be called after configuring all apps,
        importing all controllers and setting up all routes.
        """
        if self.env == 'interpreter':  return
        # If the virtual_hosts dict contains at least one entry,
        # make the root a virtual host dispatcher.
        if self.virtual_hosts:
            # vh = cherrypy.dispatch.VirtualHost(**self.virtual_hosts)
            from .virtualhost import VirtualHost
            vh = VirtualHost(self.virtual_hosts)
            warning_app = cherrypy.tree.mount(WarningController(), "/",
                                {b"/": {b"request.dispatch": vh}})
            enable_evalexception_if_configured(warning_app)
        #cherrypy.server.start()
        #cherrypy.engine.start()
        cherrypy.quickstart()
        #cherrypy.quickstart(self.app, config=None)



class Launcher(Loader):
    '''Easiest, highest level way of starting a Webpyte server or
    interactive interpreter.
    '''
    envs = ('dev', 'prod', 'interpreter')
    
    def __init__(self, executing, apps_args, interpreter_imports=''):
        if executing:
            env = self._demand_env()
        else: # the launch module was imported in an interpreter
            env = 'interpreter'
        super(Launcher, self).__init__(env)
        if env != 'interpreter':  self.make_stop_scripts()
        self.load(apps_args)
        if env != 'interpreter':
            self.start()
        else:
            exec(interpreter_imports)
        if executing and env == 'interpreter': # start IPython
            from IPython.Shell import IPShellEmbed
            ipshell = IPShellEmbed()
            ipshell()
    
    def _demand_env(self):
        """Demand a choice of environment at the command line"""
        env = sys.argv[1] if len(sys.argv) > 1 else None
        if env == 'i':  return 'interpreter'
        if env in self.envs:
            return env
        else:
            print("""
    You must specify an environment (development, production or interpreter).
        Usage:    {0} [dev | prod | i]
        Example:  {0} dev
""".format(sys.argv[0]))
            sys.exit(2)
    
    



class WarningController(object):
    @cherrypy.expose
    def index(self):
        return "You have reached the root of a Webpyte virtual host. " \
            "You should find applications in subfolders and in other domains."
